﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Windows;
using System.Windows.Shapes;
using Microsoft.Research.Kinect;
using Microsoft.Research.Kinect.Nui;

namespace KINECTUtility
{
    public class AreaDefinition
    {
        public int Index { get; set; }
        public int IndexVertical { get; set; }
        public int IndexHorizontal { get; set; }
        public double Top { get; set; }
        public double Left { get; set; }
        public double Height { get; set; }
        public double Width { get; set; }
        public Shape Target { get; set; }
        public bool Held { get; set; }
        public bool Pushed { get; set; }
    }

    public class AreaCheckerUpdateEventArgs
    {
        public TimeSpan delta { get; set; }
        public bool MoveToLeft { get; set; }
        public bool MoveToRight { get; set; }
        public bool MoveToTop { get; set; }
        public bool MoveToBottom { get; set; }
        public int MoveHorizontalCells { get; set; }
        public int MoveVerticalCells { get; set; }
        public JointID MovedJoint { get; set; }
        public int CurrentIndexHorizontal { get; set; }
        public int CurrentIndexVertical { get; set; }
    }

    public class AreaCheckerHoldEventArgs
    {
        public JointID HoldJoint { get; set; }
        public int HoldIndex { get; set; }
        public int HoldIndexHorizontal { get; set; }
        public int HoldIndexVertical { get; set; }
    }

    public class AreaCheckerPushEventArgs
    {
        public JointID PushJoint { get; set; }
        public int PushIndex { get; set; }
        public int PushIndexHorizontal { get; set; }
        public int PushIndexVertical { get; set; }
    }

    public class AreaChecker
    {
        private double WindowWidth;
        private double WindowHeight;

        /// <summary>
        /// エリアの単位幅
        /// </summary>
        public double UnitWidth { get; set; }
        /// <summary>
        /// エリアの単位高さ
        /// </summary>
        public double UnitHeight { get; set; }

        private DateTime lastChecked;
        private DateTime checkStarted;

        private List<AreaDefinition> myAreaDefs = new List<AreaDefinition>();
        private Dictionary<JointID, AreaDefinition> jointOccuping = new Dictionary<JointID, AreaDefinition>();
        private Dictionary<JointID, AreaDefinition> jointOccupided = new Dictionary<JointID, AreaDefinition>();
        private Dictionary<JointID, double> jointDepth = new Dictionary<JointID, double>();

        /// <summary>
        /// 判定する画面領域横方向の拡大・縮小パラメータ
        /// 例えば手で領域を選択する場合、KINECTがセンスする空間が大きすぎる為、
        /// この係数で手の付近のみを検知するなどの調整が出来る
        /// </summary>
        public double AdjustCoefX { get; set; }
        /// <summary>
        /// 判定する画面領域縦方向の拡大・縮小パラメータ
        /// </summary>
        public double AdjustCoefY { get; set; }

        public int MaxHorizontalIndex { get; set; }
        public int MaxVerticalIndex { get; set; }

        public double PushDepth
        {
            get { return pushDepth; }
            set { PushDepth = value; }
        }

        private double checkMarginWidth;
        private double checkMarginHeight;
        private double pushDepth = 0.2;

        /// <summary>
        /// エリアからの出を検出する為の小ぶりな四角形の幅
        /// </summary>
        public double CheckWidth
        {
            get { return checkMarginWidth * 2; }
            set { checkMarginWidth = value / 2; }
        }
        /// <summary>
        /// エリアからの出を検出する為の小ぶりな四角形の高さ
        /// </summary>
        public double CheckHeight
        {
            get { return checkMarginHeight * 2; }
            set { checkMarginHeight = value / 2; }
        }

        public AreaChecker(double windowWidth, double windowHeight, double unitWidth, double unitHeight)
        {
            WindowHeight = windowHeight;
            WindowWidth = windowWidth;
            UnitHeight = unitHeight;
            UnitWidth = unitWidth;

            CheckWidth = UnitWidth / 2;
            CheckHeight = UnitHeight / 2;

            AdjustCoefX = 1;
            AdjustCoefY = 1;
            lastChecked = DateTime.Now;

            MaxHorizontalIndex = 0;
            MaxVerticalIndex = 0;

            int index = 0;
            int indexV = 0;
            while (indexV * unitHeight <= windowHeight)
            {
                double height = unitHeight;
                if (indexV * unitHeight + unitHeight > windowHeight)
                {
                    height = windowHeight - indexV * unitHeight;
                }
                int indexH = 0;
                while (indexH * unitWidth <= windowWidth)
                {
                    double width = unitWidth;
                    if (indexH * unitWidth + unitWidth > windowWidth)
                    {
                        width = indexH * unitWidth + unitWidth - windowWidth;
                    }
                    myAreaDefs.Add(new AreaDefinition()
                    {
                        Index = index++,
                        Top = indexV * unitHeight,
                        Left = indexH * unitWidth,
                        Height = unitHeight,
                        Width = unitWidth,
                        IndexVertical = indexV,
                        IndexHorizontal = indexH,
                        Held = false
                    });
                    indexH++;
                }
                MaxHorizontalIndex = indexH;
                indexV++;
                MaxVerticalIndex = indexV;
            }
        }

        /// <summary>
        /// 判定したいJointIDの登録
        /// </summary>
        /// <param name="jointId"></param>
        public void RegistCheckJointID(JointID jointId)
        {
            jointOccuping.Add(jointId, null);
            jointOccupided.Add(jointId, null);
            jointDepth.Add(jointId, 0);
        }

        /// <summary>
        /// 判定したいJointIDの削除
        /// </summary>
        /// <param name="jointId"></param>
        public void RemoveCheckJointID(JointID jointId)
        {
            if (jointOccupided.ContainsKey(jointId))
            {
                jointOccuping.Remove(jointId);
                jointOccupided.Remove(jointId);
            }
        }

        /// <summary>
        /// 指定したインデックスに相当するエリアを返す
        /// </summary>
        /// <param name="indexV"></param>
        /// <param name="indexH"></param>
        /// <returns></returns>
        public AreaDefinition GetArea(int indexV, int indexH)
        {
            AreaDefinition areaDef = null;
            foreach (var ad in myAreaDefs)
            {
                if (ad.IndexVertical == indexV && ad.IndexHorizontal == indexH)
                {
                    areaDef = ad;
                    break;
                }
            }
            return areaDef;
        }

        public int? GetAreaIndex(int indexV, int indexH)
        {
            int? index = null;
            var areaDef = GetArea(indexV, indexH);
            if (areaDef != null)
            {
                index = areaDef.Index;
            }
            return index;
        }

        public List<AreaDefinition> GetDefAreas()
        {
            return myAreaDefs;
        }

        /// <summary>
        /// KINECTのSkeletonFrameReadyを受信したら、以下のシーケンスでCall
        /// BeginCheck()
        /// IsIncludePoint() 1..*
        /// EndCheck()
        /// </summary>
        public void BeginCheck()
        {
            checkStarted = DateTime.Now;
        }

        /// <summary>
        /// 指定された座標を含むエリア定義を返す。
        /// ここではSDKのスムージング機能は使わず、エリア定義より小ぶりな四角形を想定し、
        /// 一旦あるエリアを占めたら、その四角形がエリアを完全に出るまでは、占めているエリアに留まっている
        /// と判断するようにしている。
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="jointId"></param>
        /// <returns></returns>
        public AreaDefinition IsIncludePoint(double x, double y, JointID jointId, double z=0)
        {
            AreaDefinition result = null;
            if (!jointOccuping.ContainsKey(jointId))
            {
                return result;
            }
            x = AdjustSize(x, AdjustCoefX);
            y = AdjustSize(y, AdjustCoefY);
            double winX = (WindowWidth + WindowWidth * x) / 2;
            double winY = (WindowHeight - WindowHeight * y) / 2;

            if (jointOccupided[jointId] == null)
            {
                foreach (var area in myAreaDefs)
                {
                    if (area.Left <= winX && winX <= area.Left + area.Width)
                    {
                        if (area.Top <= winY && winY <= area.Top + area.Height)
                        {
                            result = area;
                            jointOccuping[jointId] = area;
                            jointDepth[jointId] = z;
                            result.Pushed = false;
                            break;
                        }
                    }
                }
            }
            else
            {
                bool verticalMoved = false;
                bool horizontalMoved = false;

                var occupided = jointOccupided[jointId];
                if (winX < occupided.Left)
                {
                    if (winX + this.checkMarginWidth < occupided.Left)
                    {
                        horizontalMoved = true;
                    }
                }
                else
                {
                    if (occupided.Left + occupided.Width < winX)
                    {
                        if (occupided.Left + occupided.Width + this.checkMarginWidth < winX)
                        {
                            horizontalMoved = true;
                        }
                    }
                }
                if (winY < occupided.Top)
                {
                    if (winY + this.checkMarginHeight < occupided.Top)
                    {
                        verticalMoved = true;
                    }
                }
                else
                {
                    if (occupided.Top + occupided.Height < winY)
                    {
                        if (occupided.Top + occupided.Height + this.checkMarginHeight < winY)
                        {
                            verticalMoved = true;
                        }
                    }
                }
                if (verticalMoved == false && horizontalMoved == false)
                {
                    result = occupided;
                    jointOccuping[jointId] = occupided;
                    if (jointDepth[jointId] - z > this.PushDepth)
                    {
                        result.Pushed = true;
                    }
                }
                else
                {
                    jointDepth[jointId] = z;
                    foreach (var area in myAreaDefs)
                    {
                        if (verticalMoved && horizontalMoved)
                        {
                            if (area.Left <= winX && winX <= area.Left + area.Width)
                            {
                                if (area.Top <= winY && winY <= area.Top + area.Height)
                                {
                                    result = area;
                                    jointOccuping[jointId] = area;
                                    break;
                                }
                            }
                        }
                        else
                        {
                            if (verticalMoved)
                            {
                                if (area.IndexHorizontal == occupided.IndexHorizontal)
                                {
                                    if (area.Top <= winY && winY <= area.Top + area.Height)
                                    {
                                        result = area;
                                        jointOccuping[jointId] = area;
                                        break;
                                    }
                                }
                            }
                            if (horizontalMoved)
                            {
                                if (area.IndexVertical == occupided.IndexVertical)
                                {
                                    if (area.Left <= winX && winX <= area.Left + area.Width)
                                    {
                                        result = area;
                                        jointOccuping[jointId] = area;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            if (result == null)
            {
                jointOccuping[jointId] = null;
            }
            return result;
        }

        /// <summary>
        /// 一箇所にじっとしているのを検知する時間
        /// nullなら検知しない
        /// </summary>
        public TimeSpan HoldTime { get; set; }
        private Dictionary<JointID, DateTime> holdCheckTimes = new Dictionary<JointID, DateTime>();
 //       private Dictionary<JointID, AreaDefinition> holdingArea = new Dictionary<JointID, AreaDefinition>();

        /// <summary>
        /// IsIncludePointメソッドで判定された手の位置を元に、セル移動、停止があったかを判定し、各種イベントを生成する
        /// </summary>
        public void EndCheck()
        {
            foreach (var key in jointOccuping.Keys)
            {
                if (jointOccupided.ContainsKey(key))
                {
                    var currentArea = jointOccuping[key];
                    if (currentArea != null)
                    {
                        var lastArea = jointOccupided[key];
                        if (lastArea != null)
                        {
                            int deltaH = currentArea.IndexHorizontal - lastArea.IndexHorizontal;
                            int deltaV = currentArea.IndexVertical - lastArea.IndexVertical;

                            if (deltaH != 0 || deltaV != 0)
                            {
                                if (HoldTime != null)
                                {
                                    if (lastArea.Held)
                                    {
                                        lastArea.Held = false;
                                        if (AreaUnhold != null)
                                        {
                                            AreaUnhold(new AreaCheckerHoldEventArgs()
                                            {
                                                HoldJoint = key,
                                                HoldIndex = lastArea.Index,
                                                HoldIndexHorizontal = lastArea.IndexHorizontal,
                                                HoldIndexVertical = lastArea.IndexVertical
                                            });
                                        }
                                    }
                                }
                                if (AreaMoved != null)
                                {
                                    var eventArgs = new AreaCheckerUpdateEventArgs()
                                    {
                                        MovedJoint = key,
                                        MoveHorizontalCells = deltaH,
                                        MoveVerticalCells = deltaV,
                                        delta = checkStarted - lastChecked,
                                        MoveToTop = false,
                                        MoveToBottom = false,
                                        MoveToLeft = false,
                                        MoveToRight = false,
                                        CurrentIndexHorizontal = currentArea.IndexHorizontal,
                                        CurrentIndexVertical = currentArea.IndexVertical
                                    };

                                    if (deltaH < 0) eventArgs.MoveToLeft = true;
                                    if (deltaH > 0) eventArgs.MoveToRight = true;
                                    if (deltaV < 0) eventArgs.MoveToTop = true;
                                    if (deltaV > 0) eventArgs.MoveToBottom = true;

                                    AreaMoved(eventArgs);
                                }
                            }
                            else // deltaH==0 && deltaV==0
                            {
                                if (currentArea.Pushed)
                                {
                                    if (AreaPushed != null)
                                    {
                                        AreaPushed(
                                            new AreaCheckerPushEventArgs()
                                            {
                                                PushIndex = currentArea.Index,
                                                PushJoint = key,
                                                PushIndexHorizontal = currentArea.IndexHorizontal,
                                                PushIndexVertical = currentArea.IndexVertical
                                            });
                                    }
                                }
                                if (HoldTime != null)
                                {
                                    if (holdCheckTimes.ContainsKey(key))
                                    {
                                        if (!currentArea.Held)
                                        {
                                            TimeSpan duration = DateTime.Now - holdCheckTimes[key];
                                            if (duration > HoldTime)
                                            {
                                                currentArea.Held = true;
                                                if (AreaHold != null)
                                                {
                                                    AreaHold(new AreaCheckerHoldEventArgs()
                                                    {
                                                        HoldJoint = key,
                                                        HoldIndex = currentArea.Index,
                                                        HoldIndexHorizontal = currentArea.IndexHorizontal,
                                                        HoldIndexVertical = currentArea.IndexVertical
                                                    });
                                                }

                                                holdCheckTimes.Remove(key);
                                            }
                                        }
                                    }
                                    else
                                    {
                                        if (!currentArea.Held)
                                        {
                                            holdCheckTimes.Add(key, DateTime.Now);
                                        }
                                    }
                                }
                            }
                        }
                        jointOccupided[key] = jointOccuping[key];
                    }
                }
            }

            lastChecked = checkStarted;
        }

        public delegate void AreaCheckerUpdateEventHandler(AreaCheckerUpdateEventArgs e);
        public event AreaCheckerUpdateEventHandler AreaMoved;

        public delegate void AreaCheckerHoldEventHandler(AreaCheckerHoldEventArgs e);
        public event AreaCheckerHoldEventHandler AreaHold;
        public event AreaCheckerHoldEventHandler AreaUnhold;

        public delegate void AreaCheckerPushEventHandler(AreaCheckerPushEventArgs e);
        public event AreaCheckerPushEventHandler AreaPushed;

        private double AdjustSize(double x, double coef)
        {
            double result = x / coef;
            if (x <= -1)
            {
                result = -1;
            }
            else if (x >= 1)
            {
                result = 1;
            }
            return result;
        }
    }
}
